package jmine.tec.report.impl;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import jmine.tec.report.DataCellTransformation;
import jmine.tec.report.DataRow;
import jmine.tec.report.cell.DataCell;
import jmine.tec.report.cell.FunctionDataCell;

/**
 * Classe abstrata para ajudar na construção de DataRows. Implementa os métodos relativos à adição, leitura e remoção de DataRows e
 * DataCells. Implementa o Sort.
 * 
 * @author julien
 */
public abstract class AbstractDataRow implements DataRow, Serializable {

    /**
     * O Row parente deste Row. Se for null significa que este row é 'root'.
     */
    private DataRow parentRow;

    /**
     * A lista com os DataRow's filhos deste DataRow. O propósito deste List é que um DataRow que representa um Portfolio tem um List de
     * DataRow's que representam as Carteiras do Portfolio. De maneira similar, um DataRow que representa uma Carteira tem um List de
     * DataRow's que representam os Instrumentos de sua composição.
     */
    private List<DataRow> childRows;

    /**
     * A lista com os DataCell's para este DataRow.
     */
    private List<DataCell> cells;

    /**
     * C'tor. Deve ser chamado pelas implementações.
     */
    public AbstractDataRow() {
        this.childRows = new ArrayList<DataRow>();
        this.cells = new ArrayList<DataCell>();
    }

    /**
     * C'tor
     * 
     * @param cells {@link List}
     * @param rows {@link List}
     */
    protected AbstractDataRow(final List<DataCell> cells, final List<DataRow> rows) {
        this.cells = cells;
        this.childRows = rows;
    }

    /**
     * Adiciona um DataCell na lista de DataCell's. Se o argumento for null lança um NullPointerException.
     * 
     * @param cell DataCell
     */
    public void addCell(final DataCell cell) {
        this.throwNPEIfNull(cell);
        this.getCells().add(cell);
    }

    /**
     * Adiciona um List de DataCell's na lista de DataCell's. Se o argumento for null lança um NullPointerException.
     * 
     * @param dataCells List
     */
    public void addCells(final List<? extends DataCell> dataCells) {
        this.throwNPEIfNull(dataCells);
        this.getCells().addAll(dataCells);
    }

    /**
     * Adiciona um DataRow à lista de DataRows filhos. Se o argumento for null lança um NullPointerException.
     * 
     * @param row DataRow
     */
    public void addChildRow(final DataRow row) {
        this.throwNPEIfNull(row);
        row.setParentRow(this);
        this.childRows.add(row);
    }

    /**
     * Adiciona um DataRow à lista de DataRows filhos. Se o argumento for null lança um NullPointerException.
     * 
     * @param rows DataRow
     */
    public void addChildRows(final List<? extends DataRow> rows) {
        this.throwNPEIfNull(rows);
        for (DataRow row : rows) {
            this.addChildRow(row);
        }
    }

    /**
     * @return int
     * @see br.com.maps.util.report.DataRow#getCellCount()
     */
    public int getCellCount() {
        return this.cells.size();
    }

    /**
     * @param index int
     * @return {@link DataCell}
     * @see br.com.maps.util.report.DataRow#removeCellWithIndex(int)
     */
    public DataCell removeCellWithIndex(final int index) {
        return this.cells.remove(index);
    }

    /**
     * Retorna o DataCell com o índice especificado. Lança IndexOutOfBoundsException se o índice for inválido.
     * 
     * @param index int
     * @return DataCell
     */
    public DataCell getCell(final int index) {
        final DataCell dataCell = this.getCells().get(index);
        if (dataCell instanceof FunctionDataCell) {
            return this.evalFunctionDataCell(index, this.childRows, (FunctionDataCell) dataCell);
        }
        return dataCell;
    }

    /**
     * Retorna a lista dos DataCell's deste DataRow. Se não tiver nenhum DataCell deve retornar uma lista vazia.
     * 
     * @return List
     */
    public List<DataCell> getCells() {
        return this.cells;
    }

    /**
     * Retorna o DataRow com o índice especificado. Lança IndexOutOfBoundsException se o índice for inválido.
     * 
     * @param index int
     * @return DataRow
     */
    public DataRow getChildRow(final int index) {
        return this.getChildRows().get(index);
    }

    /**
     * Retorna a lista dos DataRow's filhos deste DataRow. Se não tiver nenhum DataRow filho deve retornar uma lista vazia.
     * 
     * @return List
     */
    public List<DataRow> getChildRows() {
        return this.childRows;
    }

    /**
     * Remove um DataCell da lista de DataCell's. Se o argumento for null lança um NullPointerException.
     * 
     * @param cell DataCell
     */
    public void removeCell(final DataCell cell) {
        this.throwNPEIfNull(cell);
        this.getCells().remove(cell);
    }

    /**
     * Remove um DataRow da lista de DataRows filhos. Se o argumento for null lança um NullPointerException.
     * 
     * @param row DataRow
     */
    public void removeChildRow(final DataRow row) {
        this.throwNPEIfNull(row);
        this.getChildRows().remove(row);
    }

    /**
     * Devolve uma "visão" deste DataRow. Esta "visão" deve ser serializável e não deve conter referências para objetos do modelo.
     * 
     * @return DataRow
     */
    public final SimpleDataRow toView() {
        final SimpleDataRow ret = new SimpleDataRow(this.getNameDataCell());
        for (final DataRow row : this.getChildRows()) {
            ret.addChildRow(row.toView());
        }
        final List<DataCell> childCells = this.getCells();
        final int n = childCells.size();
        for (int i = 0; i < n; i++) {
            final DataCell cell = childCells.get(i);
            if (cell instanceof FunctionDataCell) {
                final List<DataRow> childrenRows = ret.getChildRows();
                final FunctionDataCell func = (FunctionDataCell) cell;
                final DataCell eval = this.evalFunctionDataCell(i, childrenRows, func);
                ret.addCell(eval);
            } else {
                ret.addCell(cell);
            }
        }
        return ret;
    }

    /**
     * Substitui o DataRow usando o DataCellTransformation
     * 
     * @param trans o {@link DataCellTransformation} que midifica o {@link DataCell} atual
     * @return o {@link DataRow} transformado
     */
    public DataRow transform(DataCellTransformation trans) {
        final SimpleDataRow ret = new SimpleDataRow(this.getNameDataCell());

        for (DataCell cell : this.getCells()) {
            ret.addCell(trans.transformCell(cell));
        }

        return ret;
    }

    /**
     * Evaluates a function data cell
     * 
     * @param i the index
     * @param childrenRows the children rows
     * @param func the function
     * @return {@link DataCell}
     */
    private DataCell evalFunctionDataCell(final int i, final List<DataRow> childrenRows, final FunctionDataCell func) {
        final List<DataCell> children = this.collectChildCells(i, childrenRows);
        final DataCell eval = func.eval(children);
        return eval;
    }

    /**
     * Procura e devolve as {@link DataCell} das linhas filhas
     * 
     * @param index the index
     * @param list the childRows
     * @return {@link List}
     */
    private List<DataCell> collectChildCells(final int index, final List<DataRow> list) {
        if (list.isEmpty()) {
            return Collections.emptyList();
        }
        final List<DataCell> toRet = new ArrayList<DataCell>(list.size());
        for (final DataRow row : list) {
            toRet.add(row.getCell(index));
        }
        return toRet;
    }

    /**
     * Retorna uma representa��o em String deste DataRow
     * 
     * @return String
     */
    @Override
    public String toString() {
        return this.getName();
    }

    /**
     * Lan�a um NPE se algum dos argumentos for null.
     * 
     * @param objs Object[]
     */
    private void throwNPEIfNull(final Object... objs) {
        for (final Object o : objs) {
            if (o == null) {
                throw new NullPointerException("Argumento nulo.");
            }
        }
    }

    /**
     * @return int
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((this.cells == null) ? 0 : this.cells.hashCode());
        result = prime * result + ((this.childRows == null) ? 0 : this.childRows.hashCode());
        return result;
    }

    /**
     * @param obj {@link Object}
     * @return boolean
     */
    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (this.getClass() != obj.getClass()) {
            return false;
        }
        final AbstractDataRow other = (AbstractDataRow) obj;
        if (this.cells == null) {
            if (other.cells != null) {
                return false;
            }
        } else if (!this.cells.equals(other.cells)) {
            return false;
        }
        if (this.childRows == null) {
            if (other.childRows != null) {
                return false;
            }
        } else if (!this.childRows.equals(other.childRows)) {
            return false;
        }
        return true;
    }

    /**
     * @return boolean
     * @see br.com.maps.util.report.DataRow#isLeafRow()
     */
    public final boolean isLeafRow() {
        return this.childRows.isEmpty();
    }

    /**
     * @param index int
     * @return {@link List}
     * @see br.com.maps.util.report.DataRow#getLeafCellsWithIndex(int)
     */
    public List<DataCell> getLeafCellsWithIndex(final int index) {
        if (this.isLeafRow()) {
            return Collections.singletonList(this.getCell(index));
        }
        final List<DataCell> list = new ArrayList<DataCell>(this.childRows.size());
        for (final DataRow child : this.childRows) {
            list.addAll(child.getLeafCellsWithIndex(index));
        }
        return list;
    }

    /**
     * @see br.com.maps.util.report.DataRow#removeAllCells()
     */
    public void removeAllCells() {
        this.cells.clear();
    }

    /**
     * @return {@link Iterable}
     * @see br.com.maps.util.report.DataRow#dataCellIterable()
     */
    public Iterable<? extends DataCell> dataCellIterable() {
        return new ArrayList<DataCell>(this.cells);
    }

    /**
     * @return {@link Iterable}
     * @see br.com.maps.util.report.DataRow#dataRowIterable()
     */
    public Iterable<? extends DataRow> dataRowIterable() {
        return new ArrayList<DataRow>(this.childRows);
    }

    /**
     * @return the parentRow
     */
    public DataRow getParentRow() {
        return this.parentRow;
    }

    /**
     * @param parentRow the parentRow to set
     */
    public void setParentRow(final DataRow parentRow) {
        this.parentRow = parentRow;
    }

}
